package com.atguigu.tingshu.account.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.tingshu.account.mapper.UserAccountDetailMapper;
import com.atguigu.tingshu.account.mapper.UserAccountMapper;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.model.account.UserAccount;
import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.atguigu.tingshu.vo.account.AccountLockResultVo;
import com.atguigu.tingshu.vo.account.AccountLockVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.List;

@Slf4j
@Service
@SuppressWarnings({"unchecked", "rawtypes"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {

	@Autowired
	private UserAccountMapper userAccountMapper;

    @Autowired
    private UserAccountDetailMapper userAccountDetailMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;


    /**
     * 锁定用户账户金额
     * @param accountLockVo 锁定数据对象
     * @return
     */
    @Override
    public Result<AccountLockResultVo> checkAndLockAccountMoney(AccountLockVo accountLockVo) {
        //1.根据传来的参数，获取数据
        //订单号
        String orderNo = accountLockVo.getOrderNo();
        //用户id
        Long userId = accountLockVo.getUserId();
        //锁定金额
        BigDecimal amount = accountLockVo.getAmount();
        //锁定内容
        String content = accountLockVo.getContent();

        //2.openfeign即远程服务调用的重试机制会导致幂等性问题，因此我们在这里要加分布式锁，防止账户重复锁定金额
        // 我们这里也采用redis来实现分布式锁，但是不采用传统的setnx，而是使用list
        //2.1 校验锁
        //获取对于键名的list数据类型中的所有数据
        String feignRetryKey = "feign:retry:msg:container" + orderNo;

        List<String> range = redisTemplate.opsForList().range(feignRetryKey, -1, 0);
        //进行校验，如果当前返回的所有数据中，存在同名的orderNo，那么就手动返回，出现重复锁订单出现
        if (range.contains(orderNo)){
            return Result.build(null,ResultCodeEnum.ACCOUNT_LOCK_REPEAT);
        }

        try {
            //2.2 进来先加锁(我们往redis的list类型的数据中加入一条数据)
            // list类型的leftpush方法会返回一个值，是当前对应键名的list的数据数量
            Long num = redisTemplate.opsForList().leftPush(feignRetryKey, orderNo);
            // 2.3当num为1的时候，那么就说明我们加锁成功了，如果num大于1，则说明有多个线程进来执行了leftpush，那么这就会导致重复锁定
            if (num == 1){
                //如果num等于1，那么就说明对于当前的订单，是第一次进行用户账户锁订单金额的操作，那么就允许执行如下逻辑
                //2.4 进行用户account的账户金额锁定
                userAccountMapper.checkAndLockAccountMoney(userId,amount);

                //2.5 准备返回对象数据
                AccountLockResultVo accountLockResultVo = new AccountLockResultVo();
                accountLockResultVo.setUserId(userId);
                accountLockResultVo.setAmount(amount);
                accountLockResultVo.setContent(content);

                //2.6 将accountLockResultVo对象存入redis中，
                // 方便我们后续对锁订单进行解锁，以及真正扣减余额
                // 在我们真正的去扣减用户的余额的时候，我们就可以根据这里存入redis中的数据，将锁定的金额数据对象从redis中取出
                // 同时删除掉，来进行解锁以及redis的空间释放
                String accountLockResultKey = "user:account:lockmoney:data:" + orderNo;
                redisTemplate.opsForValue().set(accountLockResultKey, JSONObject.toJSONString(accountLockResultVo));

                //2.7 保存订单流水【即对user_account_detail表做数据插入】
                saveUserAccountDetail(SystemConstant.ACCOUNT_TRADE_TYPE_LOCK,userId,"锁定操作:" + content,amount,orderNo);

                //2.8 将数据返回
                return Result.ok(accountLockResultVo);
            } else {
                //2.9 若num此时不为1，或为2甚至更大，那么就说明发生了重复订单提交，我们需要直接return回去
                //通知出现了账户的重复锁定
                return Result.build(null, ResultCodeEnum.ACCOUNT_LOCK_REPEAT);
            }
        } catch (Exception e) {
            //3.如果在上述代码执行的过程中，捕获到了异常，那么我们就需要返回订单锁定错误
            //同时！！！很重要，要记得解锁，即删除清空redis对应键名为feignRetryKey的List数据类型数据
            //如果发生了错误，且没有解锁，那么就会导致死锁，导致当前订单无法再次进行订单的锁定操作
            redisTemplate.delete(feignRetryKey);
            return Result.build(null,ResultCodeEnum.ACCOUNT_LOCK_ERROR);
        }
    }

    /**
     * 保存用户账户流水明细表
     * @param userId 用户id
     * @param orderNo 订单号
     */
    public void saveUserAccountDetail(String tradeType,Long userId,String content, BigDecimal amount, String orderNo) {
        UserAccountDetail userAccountDetail = new UserAccountDetail();
        userAccountDetail.setUserId(userId);
        userAccountDetail.setTitle(content);
        userAccountDetail.setTradeType(tradeType);
        userAccountDetail.setAmount(amount);
        userAccountDetail.setOrderNo(orderNo);

        userAccountDetailMapper.insert(userAccountDetail);
    }


    /**
     * 根据用户id获取当前用户的可用余额
     * @param userId 用户id
     * @return 可用余额
     */
    @Override
    public BigDecimal getAvailableAmount(Long userId) {
        LambdaQueryWrapper<UserAccount> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserAccount::getUserId,userId);
        UserAccount userAccount = userAccountMapper.selectOne(wrapper);
        return userAccount.getAvailableAmount();
    }
}
